package objects;

import containers.Assembly;
import containers.Canvas;
import containers.Simulation;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics2D;
import java.awt.geom.Point2D;


/**
 * 
 * @author Robert C. Duvall, edited by rwb10 & sab60
 */

public class Mass {
    // constants
    // some can be changed by the environment.xsp file
    // all have default values
    private static double ourViscosityMagnitude = 0.005;
    private static double ourGravityMagnitude = 1;
    private static double ourGravityDirection = 90;
    private static double ourCenterOfMassMagnitude = 0.005;
    private static double ourCenterOfMassExponent = 2.0;
    private static boolean ourGravityOn = true;
    private static boolean ourViscosityOn = false;
    private static boolean ourCenterOfMassOn = false;
    private static final int MASS_SIZE = 16;
    private Color myColor = Color.BLACK;

    // state
    private Point2D myCenter;
    private Force myVelocity;
    private Dimension mySize;
    private int myID;
    private double myMass;
    private Force myAcceleration;

    // relationships
    private Assembly myAssembly;

    /**
     * @param id Identification number for the Mass.
     * @param x Initial x-coordinate.
     * @param y Initial y-coordinate.
     * @param mass Mass value of the Mass.
     */
    public Mass (int id, double x, double y, double mass) {
        myAcceleration = new Force();
        myMass = mass;
        myID = id;
        setCenter(x, y);
        setVelocity(0, 0);
        mySize = new Dimension(MASS_SIZE, MASS_SIZE);
    }

    /**
     * 
     * @param pen The pen to paint with.
     */
    public void paint (Graphics2D pen) {
        Canvas.paintCircle(pen, getCenter(), MASS_SIZE, getMyColor());
    }

    /**
     * 
     * @param canvas Simulation to update to and from.
     * @param dt Change in time.
     */
    public void update (Simulation canvas, double dt) {
        applyForce(getGravity());
        applyForce(getBounce(canvas.getSize()));
        applyForce(getViscosity());
        applyForce(getWallRepulsion());
        applyForce(getCenterOfMassForce());

        // convert force back into Mover's velocity
        getVelocity().sum(myAcceleration);
        myAcceleration.reset();
        // move mass by velocity
        myCenter.setLocation(myCenter.getX() + myVelocity.getXChange() * dt,
                myCenter.getY() + myVelocity.getYChange() * dt);
    }

    /**
     * @param f force to be applied to this mass
     */
    public void applyForce (Force f) {
        if (f != null) {
            myAcceleration.sum(f);
        }
    }

    /**
     * Seems useless, but needed by Simulation.java.
     * 
     * @param id See if the ID's match.
     * @return
     */
    public boolean match (int id) {
        return myID == id;
    }

    // add gravity towards bottom
    private Force getGravity () {
        if (ourGravityOn) {
            Force result = new Force(ourGravityDirection, ourGravityMagnitude);
            result.scale(1.0 / myMass);
            return result;
        }
        return null;
    }

    private Force getViscosity () {
        if (ourViscosityOn) {
            // apply viscous force in opposite direction of velocity
            Force viscousForce = new Force(getVelocity());
            viscousForce.negate();
            viscousForce.setMagnitude(ourViscosityMagnitude * getVelocity().getMagnitude());
            return viscousForce;
        }
        return null;
    }

    // check for move out of bounds
    private Force getBounce (Dimension bounds) {
        Force impulse = new Force();
        if (getLeft() < 0) {
            impulse = new Force(0, 2);
            setCenter(getSize().width / 2, getCenter().getY());
        }
        else if (getRight() > bounds.width) {
            impulse = new Force(180, 2);
            setCenter(bounds.width - getSize().width / 2, getCenter().getY());
        }
        if (getTop() < 0) {
            impulse = new Force(270, 2);
            setCenter(getCenter().getX(), getSize().height / 2);
        }
        else if (getBottom() > bounds.height) {
            impulse = new Force(90, 2);
            setCenter(getCenter().getX(), bounds.height - getSize().height / 2);
        }
        impulse.scale(getVelocity().getRelativeMagnitude(impulse));
        return impulse;
    }

    private Force getWallRepulsion () {
        Force totalWallRepulsion = new Force(0, 0);
        for (WallRepulsion wp : myAssembly.getSimulation().getWallRepulsions()) {
            Force f = wp.getForce(this);
            totalWallRepulsion.sum(f);
        }
        return totalWallRepulsion;
    }

    private Force getCenterOfMassForce () {
        if (ourCenterOfMassOn) {
            Point2D center = myAssembly.getCenter();
            double distance = Force.distanceBetween(center, getCenter());
            double magnitude = Math.pow(distance * ourCenterOfMassMagnitude,
                    ourCenterOfMassExponent);
            double direction = Force.angleBetween(center, getCenter());
            return new Force(direction, magnitude);
        }
        else {
            return new Force(0.0, 0.0);
        }
    }

    /**
     * Returns shape's velocity.
     */
    public Force getVelocity () {
        return myVelocity;
    }

    /**
     * Resets shape's velocity.
     * 
     * @param direction The angular direction the mass is heading.
     * @param magnitude The magnitude of the mass' velocity.
     */
    public void setVelocity (double direction, double magnitude) {
        myVelocity = new Force(direction, magnitude);
    }

    /**
     * Returns shape's center.
     */
    public Point2D getCenter () {
        return myCenter;
    }

    /**
     * Resets shape's center.
     * 
     * @param x x-position
     * @param y y-position
     */
    public void setCenter (double x, double y) {
        myCenter = new Point2D.Double(x, y);
    }

    /**
     * Returns shape's left-most coordinate.
     */
    public int getLeft () {
        return (int) (getCenter().getX() - getSize().width / 2);
    }

    /**
     * Returns shape's top-most coordinate.
     */
    public int getTop () {
        return (int) (getCenter().getY() - getSize().height / 2);
    }

    /**
     * Returns shape's right-most coordinate.
     */
    public int getRight () {
        return (int) (getCenter().getX() + getSize().width / 2);
    }

    /**
     * Reports shape's bottom-most coordinate.
     * 
     * @return bottom-most coordinate
     */
    public int getBottom () {
        return (int) (getCenter().getY() + getSize().height / 2);
    }

    /**
     * Returns shape's size.
     */
    public Dimension getSize () {
        return mySize;
    }

    /**
     * Returns the mass.
     * 
     * @return
     */
    public double getMass () {
        return myMass;
    }

    /**
     * Set the gravitational force for all masses.
     * @param direction         Direction of force in degrees.
     * @param gravity           Magnitude of force.
     */
    public static void setGravity (double direction, double gravity) {
        ourGravityDirection = direction;
        ourGravityMagnitude = gravity;
    }

    /**
     * Sets the center of mass.
     * @param magnitude         Magnitude of the force.
     * @param exponent          Exponent of the force.
     */
    public static void setCenterOfMass (double magnitude, double exponent) {
        ourCenterOfMassMagnitude = magnitude;
        ourCenterOfMassExponent = exponent;
    }
    /**
     * Get's the assembly it's in.
     * @return  The assembly the mass is contained in.
     */
    public Assembly getAssembly () {
        return myAssembly;
    }
    /**
     * Put the mass in an assembly.
     * @param assembly  The assembly to put itself in.
     */
    public void setAssembly (Assembly assembly) {
        myAssembly = assembly;
    }
    /**
     * Set the viscosity for all Masses.
     * @param viscosityScalar   Viscosity scalar to set.
     */
    public static void setViscosity (double viscosityScalar) {
        ourViscosityMagnitude = viscosityScalar;
    }

    /**
     * Toggles the gravity of all masses.
     */
    public static void toggleGravity () {
        if (ourGravityOn) {
            ourGravityOn = false;
            System.out.println("Turning Gravity Off");
        }
        else {
            ourGravityOn = true;
            System.out.println("Turning Gravity On");
        }
    }

    /**
     * Toggles the viscosity of all masses.
     */
    public static void toggleViscosity () {
        if (ourViscosityOn) {
            ourViscosityOn = false;
            System.out.println("Turning Viscosity Off");
        }
        else {
            ourViscosityOn = true;
            System.out.println("Turning Viscosity On");
        }
    }

    /**
     * Toggles the center of mass of all masses.
     */
    public static void toggleCenterOfMass () {
        if (ourCenterOfMassOn) {
            ourCenterOfMassOn = false;
            System.out.println("Turning CenterOfMass Off");
        }
        else {
            ourCenterOfMassOn = true;
            System.out.println("Turning CenterOfMass On");
        }
    }

    /**
     * @return the myColor
     */
    public Color getMyColor () {
        return myColor;
    }

    /**
     * @param myNewColor the myColor to set
     */
    public void setMyColor (Color myNewColor) {
        this.myColor = myNewColor;
    }
}
